Skip to content

feat(mesh): bind dispatcher runs to agent turns - #11252

Merged
yiliang114 merged 1 commit into
codex/multi-agent-mesh-foundationfrom
codex/mesh-runtime-wiring
Sep 7, 2026
Merged

yiliang114 merged 1 commit into
codex/multi-agent-mesh-foundationfrom
codex/mesh-runtime-wiring

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

Connects the mesh dispatcher to the real background-agent turn boundary. Each mesh turn receives its persisted workspace/thread/run/attempt identity, sees the six thread tools, records correlated delivery and token usage, and terminalizes its run when the background body returns.

Launch and cold-revive prompts are committed when the runtime accepts the initial turn. Resident continuations use structured external input keyed by the run id and advance the delivery watermark only when the runtime emits the matching consumed event.

Why it's needed

The mesh rules and dispatcher were already durable, but the actual agent loop did not know which shared thread it was serving. That left the thread tools hidden and could leave successful work permanently marked running. This is the minimum runtime seam required before the two-agent live demo can run.

Reviewer Test Plan

How to verify

Review the live path for these outcomes: the first model turn can call a thread tool; a resident second turn is bound to the new thread rather than the previous one; the correlated external-message event advances accepted/consumed message ids; usage rounds land on the active attempt; and a plain-text exit becomes an unclosed run while an explicit review/wait/block close reaches a terminal state.

Evidence (Before & After)

N/A — this is runtime plumbing with no UI surface.

Tested on

OS Status
macOS ⚠️ Not run locally; the next step is the live-model demo
Windows N/A
Linux N/A

Risk & Scope

  • Main risk or tradeoff: this touches the shared background-agent turn seam, so the diff is isolated in this stacked PR and ordinary subagents keep the mesh tools hidden.
  • Not validated / out of scope: restart recovery, direct delivery into an already-running mesh turn, cancellation, REST, Web Shell, and notifications.
  • Breaking changes / migration notes: none for users; the added metadata is optional for ordinary agents.

Linked Issues

Parent delivery PR: #11206. Base: codex/multi-agent-mesh-foundation.

中文说明

这个 PR 做什么

把 mesh dispatcher 接到真实的后台 agent turn 边界。每个 mesh turn 都会拿到持久化的 workspace/thread/run/attempt 身份、看到六个 thread 工具、记录带关联 id 的消息消费与 token 用量,并在后台执行体返回时把 run 推到终态。

首次启动和冷恢复的 prompt 在 runtime 接受首轮时提交。resident 续跑使用以 run id 为关联 id 的结构化外部输入,只有 runtime 发出匹配的 consumed event 后才推进 delivery watermark。

为什么需要

mesh 的规则和 dispatcher 已经能持久化,但真实 agent loop 之前不知道自己正在处理哪条共享 thread。这会导致 thread 工具不可见,也会让成功完成的工作一直停在 running。这个 PR 只补上双 agent 真模型 demo 之前必需的最小 runtime 接缝。

Reviewer Test Plan

如何验证

检查真实链路是否满足:第一轮模型可以调用 thread 工具;resident 第二轮绑定到新 thread 而不是上一条;匹配的 external-message event 推进 accepted/consumed message ids;usage round 落到当前 attempt;纯文本退出成为 unclosed run,而显式 review/wait/block 收尾能进入终态。

证据(Before & After)

N/A——这是 runtime 接线,没有 UI。

测试环境

OS 状态
macOS ⚠️ 未在本地跑;下一步直接跑真模型 demo
Windows N/A
Linux N/A

风险与范围

  • 主要风险:改动共享的后台 agent turn 接缝,因此独立放在这个 stacked PR;普通 subagent 仍看不到 mesh 工具。
  • 未验证/不在范围:重启恢复、向正在运行的 mesh turn 直接送信、取消、REST、Web Shell、通知。
  • 迁移:用户无感;新增 metadata 对普通 agent 是可选的。

关联

主交付 PR:#11206。Base:codex/multi-agent-mesh-foundation

@yiliang114
yiliang114 merged commit 32c97a9 into codex/multi-agent-mesh-foundation Sep 7, 2026
47 of 48 checks passed
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

No admission decision — this PR merged before triage ran

Timeline, from the API:

  • 04:03:52Z — PR opened
  • 04:04:00Z — merged by @yiliang114 (admin), 8 seconds later
  • 04:08:06Z — triage posted its "running" lifecycle comment, 4 minutes after the merge

So there was nothing to gate. I am not issuing an approval or a request-changes here: on merged code an approval is a no-op and a request-changes cannot be acted on, and either would read as an admission verdict that was never actually available. The stages below are what I could still establish, because this commit reaches main through the open parent delivery PR #11206 (codex/multi-agent-mesh-foundation, 83 files) — that PR is where the gate properly applies.

Gate facts (Stage 0 / guardrails). All 17 files are core paths, so the two-tier core gate is in scope by path. It does not bite: the title is feat, which is never hard-blocked on size, and production logic lines come to ~463 (506 total − 26 in dispatch-port.test.ts − 17 in the two docs/plans files), under the 500-line maintainer-awareness trigger and well under the 1000+ advisory. The author has admin, so the change is maintainer-authored and exempt from the gate regardless. Not a fork and not a refactor, so the fork-refactor approval guardrail does not apply either.

Test evidence: none exists, and I want to be blunt about that. There are zero pull_request workflow runs on 0a807ff7d1dfd5563c19736482742552cb3d496c. Every check-run on that SHA is pull_request_target bot orchestration — precheck-pr, review-pr, tmux-testing, verify and friends, all skipped or cancelled, plus this triage run still in progress. No build, no typecheck, no lint, no unit suite ever executed against this head; the merge beat CI to it. That lines up with the PR's own words — "Tested on: macOS ⚠️ Not run locally" — and with the acceptance doc this diff edits, which says the correction "was deliberately not expanded with new test code or a local CI/build pass". Per the gate's rules I did not build or run anything myself. So the honest status is: not verified — no automated evidence exists for this commit on any platform. The one test touched, dispatch-port.test.ts, was never executed here.

What I could verify statically (reading the merge-commit tree, not running it). The central mechanism is sound, and better than I expected before reading it:

  • Tool visibility actually works. prepareTools() computes getExcludedToolsForCurrentContext() at agent-core.ts:683, and that call sits inside the agent's own AsyncLocalStorage frame, which runMeshTurnrunWithMeshRunContext establishes around bgBody. So the six thread_* tools really are un-excluded for a mesh turn and stay hidden for ordinary subagents and teammates.
  • runWithMeshRunContext throws on nesting a different run and permits re-entering an identical one. That is the right failure shape — it makes "body bound to its first thread forever" impossible rather than merely unlikely.
  • registry.continueResidentAgent(agentId, message, deliveryId?) already takes the third argument on the base, so the new call site is not a signature break.
  • finishRun in the bridge's finally block does fix the "successful work left permanently running" problem the PR set out to solve, and a non-terminal meta status becomes failed rather than an implicit success.

One finding worth carrying into #11206 — the binding is persisted before the dispatch is known to succeed. In dispatch-port.ts, start() calls bindNextTurn() before the action switch for every non-launch action, and bindNextTurn writes meshRun into the body's meta file. But the switch can still fail afterwards: continue_completed can return capacity_wait, not_completed can fail its revive into launch_failed, and resume can fail the same way. In each case the dispatcher releases the claim without spending the attempt — yet the stale meshRun for a run that never started stays on disk.

The reason this is not merely untidy: finishRunInTransaction treats queued as a finishable status (run-lifecycle.ts:398-402), so it will terminalize a released run silently rather than throwing. Any later turn on that resident body which reaches readAgentMeta(metaPath)?.meshRun without first passing through bindNextTurn would expose the thread tools to a non-mesh turn and, at turn end, close out a run that was never executed — recording closeKind: 'unclosed' or a failure against it. A subsequent mesh dispatch does rebind and overwrite, so this needs a turn that starts outside the dispatch port; I could not establish from the diff whether such a path exists for a mesh body, and I am flagging it as an open reachability question rather than asserting a live bug. Binding after the action succeeds — or rolling the binding back on any non-started result — would close it either way, and would also match the fail-loud posture the rest of the mesh code deliberately takes.

Minor, non-blocking. The six thread-tool names are now enumerated in five places: MESH_THREAD_TOOL_NAMES and MESH_TOOL_CLASSIFICATION in capability.ts, MESH_THREAD_TOOLS plus the two EXCLUDED_TOOLS_FOR_* sets in agent-core.ts. Moving capability.ts off string literals onto ToolNames.* is a genuine improvement this PR made; deriving the agent-core.ts list from the existing MESH_THREAD_TOOL_NAMES export would remove the remaining duplication so a seventh tool cannot be added to four of the five.

On direction and scope. Nothing to raise. This is the minimum seam the stated goal needs — no speculative recovery, cancellation, REST or Web Shell surface, all of which the PR explicitly puts out of scope. The isolation argument holds: ordinary subagents keep the tools hidden, and the coupling stays inside agents/mesh/ plus two small, additive registrations in config.ts and rule-parser.ts. My independent proposal before reading the diff was essentially this shape, and I expected the mesh concept to leak further into general-purpose config and permissions than it did.

Suggest the two items above — the binding-order question and a first automated pass — get picked up on #11206, where there is still a gate to satisfy and CI will actually run.

中文说明

未做准入判定 —— 本 PR 在 triage 运行前已合并

时间线(均来自 API):

  • 04:03:52Z — PR 创建
  • 04:04:00Z — 由 @yiliang114(admin)合并,距创建仅 8 秒
  • 04:08:06Z — triage 发出「正在运行」的 lifecycle 评论,此时已合并 4 分钟

因此没有可准入的对象。这里不会给出 approve 或 request-changes:对已合并的代码,approve 是空操作,request-changes 无法被执行,两者都会被误读成一个实际上并不存在的准入结论。下面各阶段是我仍能确认的内容,因为这个 commit 会通过仍处于 open 状态的主交付 PR #11206codex/multi-agent-mesh-foundation,83 个文件)进入 main —— 真正的 gate 在那个 PR 上。

Gate 事实(Stage 0 / 护栏)。 17 个文件全部落在核心路径,按路径确实触发两级核心 gate,但都不构成阻断:标题是 feat,从不因规模硬阻断;生产逻辑行约 463 行(总 506 − dispatch-port.test.ts 的 26 − 两个 docs/plans 文件的 17),低于 500 行的维护者关注阈值,也远低于 1000+ 的大 PR 提示线。作者是 admin,属于维护者自己的 PR,本就豁免该 gate。既非 fork 也非 refactor,fork-refactor 审批护栏同样不适用。

测试证据:完全不存在,这里直说。 0a807ff7d1dfd5563c19736482742552cb3d496c 上的 pull_request workflow run 数量为。该 SHA 上所有 check-run 都是 pull_request_target 的机器人编排作业 —— precheck-prreview-prtmux-testingverify 等,全部为 skippedcancelled,外加仍在进行的本次 triage。没有 build、没有 typecheck、没有 lint、没有单元测试跑过这个 head;合并比 CI 更快。这与 PR 自己的说明一致(「Tested on: macOS ⚠️ 未在本地跑」),也与本次 diff 修改的验收文档一致 —— 其中写明这次修正「刻意没有补测试代码,也没有跑本地 CI/build」。按 gate 规则我没有自行构建或运行任何东西。所以诚实的结论是:未验证 —— 该 commit 在任何平台上都没有自动化证据。 唯一被改动的测试 dispatch-port.test.ts 在此也从未执行过。

静态可确认的部分(只读合并后的代码树,未运行)。 核心机制是成立的,而且比我读 diff 之前预期的更好:

  • 工具可见性确实生效。prepareTools()agent-core.ts:683 计算 getExcludedToolsForCurrentContext(),该调用位于 agent 自身的 AsyncLocalStorage frame 内,而这个 frame 正是 runMeshTurnrunWithMeshRunContext 包住 bgBody 时建立的。所以六个 thread_* 工具在 mesh turn 中确实被解除排除,同时对普通 subagent 和 teammate 保持隐藏。
  • runWithMeshRunContext 在嵌套不同 run 时抛错,在重复进入同一 run 时放行。这是正确的失败形态 —— 它让「body 被永久绑定到第一条 thread」变成不可能,而不只是不太可能。
  • registry.continueResidentAgent(agentId, message, deliveryId?) 在 base 分支上就已接受第三个参数,所以新调用点不是签名破坏。
  • bridge 的 finally 中调用 finishRun,确实解决了 PR 想解决的「成功的工作永久停在 running」问题;非终态的 meta status 会被记为 failed,而不是隐式成功。

一个值得带到 #11206 的发现 —— binding 在 dispatch 确认成功之前就已落盘。 dispatch-port.tsstart() 对所有非 launch action 都在 switch 之前调用 bindNextTurn(),而 bindNextTurn 会把 meshRun 写入 body 的 meta 文件。但 switch 之后仍可能失败:continue_completed 可能返回 capacity_waitnot_completed 的 revive 可能失败成 launch_failedresume 同样可能失败。这些情况下 dispatcher 会释放 claim 且不消耗 attempt —— 但那个从未启动的 run 的陈旧 meshRun 仍留在磁盘上。

之所以不只是「不整洁」:finishRunInTransactionqueued 视为可终结状态(run-lifecycle.ts:398-402),因此它会静默终结一个已释放的 run,而不是抛错。任何之后在该 resident body 上、未经 bindNextTurn 就读到 readAgentMeta(metaPath)?.meshRun 的 turn,都会把 thread 工具暴露给一个非 mesh turn,并在 turn 结束时终结一个从未执行的 run —— 给它记上 closeKind: 'unclosed' 或一次失败。后续 mesh dispatch 会重新绑定并覆盖,所以这需要一条不经过 dispatch port 的 turn 启动路径;我无法从 diff 判断 mesh body 是否存在这样的路径,因此把它作为可达性待确认的问题提出,而不是断言存在线上 bug。无论可达与否,把绑定改到 action 成功之后(或在任何非 started 结果时回滚绑定)都能关掉它,也更符合 mesh 其余代码刻意采取的 fail-loud 姿态。

次要、不阻断。 六个 thread 工具名现在出现在五处:capability.tsMESH_THREAD_TOOL_NAMESMESH_TOOL_CLASSIFICATIONagent-core.tsMESH_THREAD_TOOLS 以及两个 EXCLUDED_TOOLS_FOR_* 集合。本 PR 把 capability.ts 从字符串字面量改到 ToolNames.* 是实实在在的改进;如果 agent-core.ts 的列表直接复用已有的 MESH_THREAD_TOOL_NAMES 导出,就能消除剩余重复,避免将来加第七个工具时漏改五处中的某几处。

方向与范围。 没有异议。这是达成目标所需的最小接缝 —— 没有投机性的恢复、取消、REST 或 Web Shell 面,PR 也明确把这些划到范围外。隔离论证成立:普通 subagent 看不到这些工具,耦合留在 agents/mesh/ 内,只在 config.tsrule-parser.ts 各加了一小段纯增量的注册。我在读 diff 之前的独立设想基本就是这个形状,而且我原以为 mesh 概念会比实际更多地渗入通用 config 与 permissions。

建议上面两项 —— binding 顺序问题,以及第一次自动化验证 —— 在 #11206 上补齐;那里仍有需要满足的 gate,CI 也会真正运行。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 0a807ff7d1dfd5563c19736482742552cb3d496c · re-run with @qwen-code /triage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants